/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package simple.utils.directorywatch;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author rehak
 */
public class DirectoryWatch implements Closeable {

    private static final Logger LOGGER = LoggerFactory.getLogger(DirectoryWatch.class);
    private List<WatchListener> listeners = new CopyOnWriteArrayList<>();
    private final String rootDir;
    private WatchService watcher;
    private final ReentrantLock lock = new ReentrantLock();
    private volatile boolean watching = true;
    private Map<WatchKey, Path> keys = new ConcurrentHashMap<>();
    private final boolean recursive;
    private final ExecutorService exec = Executors.newSingleThreadExecutor();

    public DirectoryWatch(String rootDir, boolean recursive) {
        this.recursive = recursive;
        this.rootDir = rootDir;
    }

    public void addListener(WatchListener listener) {
        listeners.add(listener);
    }

    public void removeListener(WatchListener listener) {
        listeners.remove(listener);
    }

    private void fireNewFileEvent(File f) {
        for (WatchListener wl : listeners) {
            wl.onFileEvent(new FileEvent(f, FileEvent.FileEventType.NEW));
        }
    }

    private void fireFileModifiedEvent(File f) {
        for (WatchListener wl : listeners) {
            wl.onFileEvent(new FileEvent(f, FileEvent.FileEventType.MODIFY));
        }
    }

    private void fireFileDeletedEvent(File f) {
        for (WatchListener wl : listeners) {
            wl.onFileEvent(new FileEvent(f, FileEvent.FileEventType.DELETE));
        }
    }

    private void processEvents() throws IOException {
        while (watching) {
            // wait for key to be signaled
            WatchKey wkey;
            try {
                wkey = watcher.take();
                LOGGER.debug("File event detected");
            } catch (InterruptedException x) {
                LOGGER.error("Interrupted exception", x);
                return;
            }

            Path path = keys.get(wkey);

            for (WatchEvent<?> event : wkey.pollEvents()) {
                WatchEvent.Kind<?> kind = event.kind();

                // This key is registered only
                // for ENTRY_CREATE events,
                // but an OVERFLOW event can
                // occur regardless if events
                // are lost or discarded.
                if (kind == StandardWatchEventKinds.OVERFLOW) {
                    continue;
                }

                // The filename is the
                // context of the event.
                WatchEvent<Path> ev = (WatchEvent<Path>) event;
                Path filename = ev.context();

                // Resolve the filename against the directory.
                // If the filename is "test" and the directory is "foo",
                // the resolved name is "test/foo".
                Path child = path.resolve(filename);
                File f = child.toFile();
                if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
                    fireNewFileEvent(f);
                    if (recursive) {
                        if (f.isDirectory()) {
                            register(f.toPath(), recursive);
                        }
                    }
                } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
                    fireFileDeletedEvent(f);
                } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
                    fireFileModifiedEvent(f);
                }
            }

            // Reset the key -- this step is critical if you want to
            // receive further watch events.  If the key is no longer valid,
            // the directory is inaccessible so exit the loop.
            boolean valid = wkey.reset();
            if (!valid) {
                LOGGER.debug("Removing watched path " + path);
                keys.remove(wkey);
                break;
            }
        }
    }

    private void register(Path path, boolean recursive) throws IOException {
        LOGGER.debug("Adding path " + path + " to watched");
        WatchKey register = path.register(watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.OVERFLOW);
        keys.put(register, path);
        if (recursive) {
            for (File f : path.toFile().listFiles()) {
                if (f.isDirectory() && !keys.values().contains(f.toPath())) {
                    register(f.toPath(), recursive);
                }
            }
        }
    }

    public void start() throws IOException {
        lock.lock();
        try {
            this.watcher = FileSystems.getDefault().newWatchService();
            watching = true;
            Path dir = new File(rootDir).toPath();
            register(dir, recursive);
            exec.submit(new Callable<Void>() {
                @Override
                public Void call() throws Exception {
                    processEvents();
                    return null;
                }
            });
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void close() {
        lock.lock();
        try {
            watching = false;
            for (WatchKey key : keys.keySet()) {
                key.cancel();
            }
            keys.clear();
            if (watcher != null) {
                watcher.close();
            }
            exec.shutdown();
        } catch (IOException ex) {
            LOGGER.error("Error closing watcher ", ex);
            throw new RuntimeException("Error closing watcher", ex);
        } finally {
            lock.unlock();
        }
    }

    @SuppressWarnings("unchecked")
    static <T> WatchEvent<T> cast(WatchEvent<?> event) {
        return (WatchEvent<T>) event;
    }
}
